Filtragem de imagens e contornos com OpenCV
Atenção: Rode este notebook com jupyter notebook e não jupyter lab. A parte interativa requer que seja específico
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
import time as t
import sys
import math
print ("OpenCV Version : %s " % cv2.__version__)
import auxiliar as aux
from ipywidgets import widgets, interact, interactive, FloatSlider, IntSlider
Usando OpenCV para facilitar nossa vida
Na última APS programamos diretamente duas operações comuns em imagens:
- limiarização: transforma uma imagem níveis de cinza em uma máscara binária. Útil para segmentar a imagem
- ajuste de constraste: "estica" o histograma da imagem, melhorando o constraste entre regiões da imagem.
O OpenCV já tem funções que fazem essas operações e, agora que já entendemos o que elas fazem, podemos usá-las para deixar nosso código mais compacto.
Ajuste de contraste
A função cv2.equaliseHist faz o ajuste de constraste em uma imagem. Veja na documentação do OpenCV mais detalhes sobre seu funcionamento.
rintin = cv2.imread("img/RinTinTin.jpg", cv2.IMREAD_GRAYSCALE)
plt.figure(figsize=(12, 8))
plt.subplot(221)
plt.imshow(rintin, cmap="gray", vmin=0, vmax=255)
plt.subplot(222)
plt.hist(rintin.flatten(), bins=256)
plt.xlim([-1,256])
rintin_eq = cv2.equalizeHist(rintin)
plt.subplot(223)
plt.imshow(rintin_eq, cmap='gray')
plt.subplot(224)
plt.hist(rintin_eq.flatten(), bins=256)
plt.xlim([-1,256])
plt.show()
Limiarização
A função cv2.threshold realiza a operação de limiarização. Veja na documentação do OpenCV mais detalhes sobre ela.
plt.figure(figsize=(12, 4))
plt.subplot(121)
plt.imshow(rintin_eq, cmap="gray", vmin=0, vmax=255)
plt.subplot(122)
ret, rintin_lim = cv2.threshold(rintin_eq, 127, 255, cv2.THRESH_BINARY)
plt.imshow(rintin_lim, cmap='gray')
plt.show()
Detecção de cores com HSV
img_color = cv2.imread("img/hall_box_battery1.jpg")
img_rgb = cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB)
img_hsv = cv2.cvtColor(img_color, cv2.COLOR_BGR2HSV)
plt.imshow(img_rgb)
plt.show()
colorpicker = widgets.ColorPicker(
concise=False,
description='Escolha uma cor',
value='#ff0e00',
disabled=False
)
colorpicker
hsv1, hsv2 = aux.ranges(colorpicker.value)
print(hsv1, hsv2)
mask = cv2.inRange(img_hsv, hsv1, hsv2)
plt.imshow(mask, cmap="gray")
plt.show()
selecao = cv2.bitwise_and(img_rgb, img_rgb, mask=mask)
plt.imshow(selecao)
plt.show()
Componentes conexos e contornos
E se tivéssemos mais de um objeto vermelho na imagem? Como faríamos para identificá-los?
Fonte das imagens: http://time.com/4299724/coca-cola-diet-coke-redesign/
coke = cv2.imread("img/coke-cans.jpg")
coke_rgb= cv2.cvtColor(coke, cv2.COLOR_BGR2RGB)
coke_hsv= cv2.cvtColor(coke, cv2.COLOR_BGR2HSV)
plt.figure(figsize=(15,10))
plt.imshow(coke_rgb)
plt.show()
Para detectar vermelho em HSV, não se esquecer de selecionar a faixa inferior e a superior do canal H
cor_menor1 = np.array([172, 50, 50])
cor_maior1 = np.array([180, 255, 255])
mask_coke1 = cv2.inRange(coke_hsv, cor_menor1, cor_maior1)
plt.figure(figsize=(15,10))
plt.imshow(mask_coke1, cmap='gray')
plt.show()
cor_menor2 = np.array([0, 50, 50])
cor_maior2 = np.array([8, 255, 255])
mask_coke2 = cv2.inRange(coke_hsv, cor_menor2, cor_maior2)
plt.figure(figsize=(15,10))
plt.imshow(mask_coke2, cmap='gray')
plt.show()
# Juntando as máscaras
mask_coke = cv2.bitwise_or(mask_coke1, mask_coke2)
plt.figure(figsize=(15,10))
plt.imshow(mask_coke, cmap="gray", vmin=0, vmax=255)
plt.show()
Componentes conexos
Após a segmentação da imagem por mascaramento, podemos observar que os pixels de interesse podem formar um ou mais grupos conectados entre si, ou seja, conjuntos de pixels que se comunicam através de algum caminho que passa apenas por pixels de interesse (brancos)
->
-> 
No OpenCV, é possível encontrar componente conexos em imagens tons de cinza através da função cv2.findContours(). Ela considera pixels de valor maior do que 0 como pixels de interesse.
Usamos a função da forma:
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
mask é a imagem com a máscara binária
- cv2.RETR_CCOMP indica que queremos organizar os contornos em componentes conexos e buracos dentro deles
- cv2.CHAIN_APPROX_NONE indica que queremos armazenar todos os pontos do contorno
- contours é uma lista de contornos, contendo os pontos a ele pertencententes
- hierarchy é uma lista indicando a organização dos contornos em termos dos componentes e de seus buracos
Os componentes conexos são representados através de seus contornos internos, ou seja, dos pixels de cada componente conexo que são vizinhos a pixels de fundo. Para desenhar os contornos em uma imagem, usamos a função cv2.drawContours(), que usamos da forma:
cv2.drawContours(imagem, contours, indice, cor)
imagem é a imagem colorida ou tons de cinza a receber o contorno
- contours é a lista de contornos obtida com cv2.findContours()
- indice é o índice do contorno dentro da lista a ser desenhado; se indice < 0 desenha todos os contornos
- cor é a cor do pixel a ser usada para desenhar o contorno
contornos, arvore = cv2.findContours(mask_coke.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
len(contornos)
contornos_img = coke_rgb.copy()
cv2.drawContours(contornos_img, contornos, -1, [0, 0, 255], 3);
plt.figure(figsize=(15,10))
plt.imshow(contornos_img)
plt.show()
Medidas dos contornos
A partir dos contornos, podemos tirar uma série de medidas como:
- Área: número de pixels petencentes ao contorno, calculada com cv2.contourArea(contour)
- Centro de massa: linha e coluna do centro de masssa do contorno
Exemplo: calcula o componente conexo de maior área e desenha seu contorno e o ponto do centro de massa
Maior contorno
maior = None
maior_area = 0
for c in contornos:
area = cv2.contourArea(c)
if area > maior_area:
maior_area = area
maior = c
cv2.drawContours(contornos_img, [maior], -1, [0, 255, 255], 5);
plt.figure(figsize=(15,10))
plt.imshow(contornos_img)
plt.show()
Centro de massa do contorno.
Jeito 1 - mais preciso
def crosshair(img, point, size, color):
""" Desenha um crosshair centrado no point.
point deve ser uma tupla (x,y)
color é uma tupla R,G,B uint8
"""
x,y = point
cv2.line(img,(x - size,y),(x + size,y),color,5)
cv2.line(img,(x,y - size),(x, y + size),color,5)
""" Retorna uma tupla (cx, cy) que desenha o centro do contorno"""
M = cv2.moments(maior)
# Usando a expressão do centróide definida em: https://en.wikipedia.org/wiki/Image_moment
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
crosshair(contornos_img, (cX,cY), 10, (0,255,0))
plt.figure(figsize=(15,10))
plt.imshow(contornos_img)
plt.show()
Jeito 2 - aproximado
ATENÇÃO: essa técnica de calcular o centro de massa diretamente do contorno dá resultados melhores quando usado com contornos convexos e cv2.CHAIN_APPROX_NONE. Em outros casos, é melhor fazer como no jeito 1.
cX = int(maior[:,:,0].mean())
cY = int(maior[:,:,1].mean())
crosshair(contornos_img, (cX,cY), 10, (255,255,0))
plt.figure(figsize=(15,10))
plt.imshow(contornos_img)
plt.show()
Prática
Exercício 1: ainda trabalhando com as imagens das latinhas, use seus conhecimentos de segmentação de imagens para obter uma máscara que tenha somente o contorno externo das 4 latinhas.
# limiar
# Tirar o fundo, se for colorido => branco usamos GRAY
gray = cv2.cvtColor(coke_rgb, cv2.COLOR_RGB2GRAY)
latas = np.zeros_like(gray)
latas[gray < 200] = 255
contornos, _ = cv2.findContours(latas, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Ordena por área e seleciona X maiores
contornos_com_tamanhos = []
for c in contornos:
contornos_com_tamanhos.append((cv2.contourArea(c), c))
def primeiro_elemento(item):
return item[0]
contornos_com_tamanhos = sorted(contornos_com_tamanhos, key=primeiro_elemento, reverse=True)
#filtrando contornos baseado em tamanho
contornos_filtrados = []
for c in contornos:
if cv2.contourArea(c) > 2000:
contornos_filtrados.append(c)
# mostra contornos colorido (debug)
import random
contornos_img = np.zeros_like(coke_rgb)
for c in contornos_com_tamanhos[:4]:
cv2.drawContours(contornos_img, [c[1]], -1, (
random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255)
), -1)
plt.imshow(contornos_img)
imagem_final = coke_rgb.copy()
cv2.drawContours(imagem_final, contornos_filtrados, -1, (255, 0, 0), 5)
plt.imshow(imagem_final)
Exercício 2: usando código, faça uma função que filtra a máscara anterior e deixa somente as latinhas da Coca-cola Life (a que tem a parte verde no topo). Dica: use as funções de contornos para analisar cada latinha individualmente. Depois disso, veja se a latinha contém uma quantidade de pixels verdes "grande".
lata_verde = np.zeros_like(gray)
lata_verde = cv2.inRange(coke_hsv, (43, 50, 50), (52, 255, 255))
saida_mask = np.zeros_like(gray)
# Opção 1: ver se a máscara de cada lata tem verde
for c in contornos_filtrados[::-1]:
uma_lata = np.zeros_like(gray)
cv2.drawContours(uma_lata, [c], -1, 255, -1)
uma_lata = cv2.bitwise_and(uma_lata, lata_verde)
# opção "certa": acha contornos e filtra área
# opção "prática":
area = np.sum(uma_lata)
if area > (255 * 10):
cv2.drawContours(saida_mask, [c], -1, 255, -1)
# Mostrar imagem a partir de máscara
# saida = coke_rgb.copy()
# saida = cv2.bitwise_and(saida, saida, mask=saida_mask)
# plt.imshow(saida)
# Opção 2: checar se o centro do verde está dentro de alguma lata
contornos_verde, _ = cv2.findContours(lata_verde, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
centros = []
for c in contornos_verde:
centros.append((c[:,:, 0].mean(), c[:, :, 1].mean()))
saida_mask2 = np.zeros_like(gray)
for c in contornos:
for cv in centros:
if cv2.pointPolygonTest(c, cv, False) > 0:
cv2.drawContours(saida_mask2, [c], -1, 255, -1)
plt.imshow(saida_mask2)
